/*
 * BSPPacket.as
 *
 * Copyright (C) 2011 - Dr.NP
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

/**
 * BSP packet ActionScript3 port
 * 
 * @package org.bsgroup.bsp.utils
 * @author Dr.NP <np@bsgroup.org>
 * @update 08/14/2012
 * @changelog 
 * 		[07/10/2012] - Creation
 * 		[08/14/2012] - _pack method, key meta bug fixed
 */
package org.bsgroup.bsp.protocol
{
	import flash.net.Socket;
	import flash.utils.ByteArray;
	import flash.utils.Endian;
	
	import org.bsgroup.bsp.inc.BSPConstants;
	import org.bsgroup.bsp.utils.Debugger;
	
	// BSP.Packet protocol operator.
	// For the well-known issue, ActionScript has no 64bit integer data type, 
	// so the Higher 4bits of int64_t will be IGNORE!
	public class BSPPacket
	{
		private const BSP_VAL_INT8		: uint = 0x1;
		private const BSP_VAL_INT16		: uint = 0x2;
		private const BSP_VAL_INT32		: uint = 0x3;
		private const BSP_VAL_INT64		: uint = 0x4;
		private const BSP_VAL_BOOLEAN	: uint = 0x5;
		private const BSP_VAL_FLOAT		: uint = 0x11;
		private const BSP_VAL_DOUBLE	: uint = 0x12;
		private const BSP_VAL_STRING	: uint = 0x21;
		private const BSP_VAL_OBJECT	: uint = 0x51;
		private const BSP_VAL_OBJECT_END: uint = 0x52;
		private const BSP_VAL_ARRAY		: uint = 0x61;
		private const BSP_VAL_ARRAY_END	: uint = 0x62;
		private const BSP_VAL_NULL		: uint = 0x7F;
		
		private var length_mark			: uint;
		private var last_cmd 			: uint = 0;
		private var last_plen			: uint = 0;
		private var has_remaining 		: Boolean = false;
		
		public function BSPPacket()
		{
			return;
		}
		
		public function unpack(data : ByteArray, mark : uint) : Object
		{
			mark = (mark == BSPConstants.PACKET_MARK_64B) ? BSPConstants.PACKET_MARK_64B : BSPConstants.PACKET_MARK_32B;
			
			return this._unpack_object(data, mark);
		}
		
		public function pack(params : Object, mark : uint = BSPConstants.PACKET_MARK_32B) : ByteArray
		{
			mark = (mark == BSPConstants.PACKET_MARK_64B) ? BSPConstants.PACKET_MARK_64B : BSPConstants.PACKET_MARK_32B;
			
			return this._serialize(params, mark);
		}
		
		private function _unpack_object(data : ByteArray, mark : uint) : Object
		{
			var r						: Object = new Object();
			var remaining				: uint = data.bytesAvailable;
			var vtype					: uint;
			var vlen					: uint;
			var vn						: Number;
			var vih						: uint;
			var vil						: uint;
			var key						: String;
			var is_key					: Boolean = true;
			
			data.endian = Endian.BIG_ENDIAN;
			
			while (remaining > 0)
			{
				vtype = data.readUnsignedByte();
				
				if (is_key)
				{
					if (vtype == BSP_VAL_STRING)
					{
						// Valid string key
						if (BSPConstants.PACKET_MARK_64B == mark)
						{
							vih = data.readInt();
						}
						vil = data.readUnsignedInt();
						// Ignore vih now
						key = data.readUTFBytes(vil);
					}
					
					else if (vtype == BSP_VAL_OBJECT_END)
					{
						return r;
					}
					
					else
					{
						// Unknown type
						break;
					}
					
					is_key = false;
					remaining = data.bytesAvailable;
					continue;
				}
				
				else
				{
					switch (vtype)
					{
						case BSP_VAL_INT8 : 
							r[key] = data.readByte();
							break;
						
						case BSP_VAL_INT16 : 
							r[key] = data.readShort();
							break;
						
						case BSP_VAL_INT32 : 
							r[key] = data.readInt();
							break;
						
						case BSP_VAL_INT64 : 
							vih = data.readInt();
							vil = data.readUnsignedInt();
							r[key] = vil;
							break;
						
						case BSP_VAL_BOOLEAN : 
							vil = data.readByte();
							r[key] = (!vil) ? false : true;
							break;
						
						case BSP_VAL_FLOAT : 
							r[key] = data.readFloat();
							break;
						
						case BSP_VAL_DOUBLE : 
							r[key] = data.readDouble();
							break;
						
						case BSP_VAL_STRING : 
							// Read length first
							if (BSPConstants.PACKET_MARK_64B == mark)
							{
								vih = data.readInt();
							}
							vil = data.readUnsignedInt();
							r[key] = data.readUTFBytes(vil);
							break;
						
						case BSP_VAL_OBJECT : 
							r[key] = _unpack_object(data, mark);
							break;
						
						case BSP_VAL_ARRAY : 
							r[key] = _unpack_array(data, mark);
							break;
						
						case BSP_VAL_NULL : 
							r[key] = null;
							break;
						
						default : 
							break;
					}
					
					is_key = true;
				}
				
				remaining = data.bytesAvailable;
			}
			
			return r;
		}
		
		private function _unpack_array(data : ByteArray, mark : uint) : Array
		{
			var r						: Array = new Array();
			var remaining				: uint = data.bytesAvailable;
			var vtype					: uint;
			var vlen					: uint;
			var vn						: Number;
			var vih						: uint;
			var vil						: uint;
			var key						: uint = 0;
			
			data.endian = Endian.BIG_ENDIAN;
			
			while (remaining > 0)
			{
				vtype = data.readUnsignedByte();
				switch (vtype)
				{
					case BSP_VAL_INT8 : 
						r[key] = data.readByte();
						break;
					
					case BSP_VAL_INT16 : 
						r[key] = data.readShort();
						break;
					
					case BSP_VAL_INT32 : 
						r[key] = data.readInt();
						break;
					
					case BSP_VAL_INT64 : 
						vih = data.readInt();
						vil = data.readUnsignedInt();
						r[key] = vil;
						break;
					
					case BSP_VAL_BOOLEAN : 
						vil = data.readByte();
						r[key] = (!vil) ? false : true;
						break;
					
					case BSP_VAL_FLOAT : 
						r[key] = data.readFloat();
						break;
					
					case BSP_VAL_DOUBLE : 
						r[key] = data.readDouble();
						break;
					
					case BSP_VAL_STRING : 
						// Read length first
						if (BSPConstants.PACKET_MARK_64B == mark)
						{
							vih = data.readInt();
						}
						vil = data.readUnsignedInt();
						r[key] = data.readUTFBytes(vil);
						break;
					
					case BSP_VAL_OBJECT : 
						r[key] = _unpack_object(data, mark);
						break;
					
					case BSP_VAL_ARRAY : 
						r[key] = _unpack_array(data, mark);
						break;
					
					case BSP_VAL_ARRAY_END : 
						return r;
						break;
					
					case BSP_VAL_NULL : 
						r[key] = null;
						break;
					
					default : 
						break;
				}
				
				remaining = data.bytesAvailable;
				key ++;
			}
			
			return r;
		}
		
		private function _serialize(input : Object, mark : uint) : ByteArray
		{
			var r : ByteArray = new ByteArray;
			r.endian = Endian.BIG_ENDIAN;
			
			if (input)
			{
				this._ins_stream_object(input, r, mark);
			}
			
			return r;
		}
		
		private function _ins_stream_object(input : Object, stream : ByteArray, mark : uint) : void
		{
			for (var key : String in input)
			{
				// Write key
				stream.writeByte(BSP_VAL_STRING);
				if (BSPConstants.PACKET_MARK_64B == mark)
				{
					stream.writeInt(0);
				}
				stream.writeUnsignedInt(key.length);
				stream.writeUTFBytes(key);
				
				if (input[key] is int)
				{
					stream.writeByte(BSP_VAL_INT32);
					stream.writeInt(input[key]);
				}
				
				else if (input[key] is Number)
				{
					stream.writeByte(BSP_VAL_DOUBLE);
					stream.writeDouble(input[key]);
				}
				
				else if (input[key] is Boolean)
				{
					stream.writeByte(BSP_VAL_BOOLEAN);
					stream.writeByte(input[key] ? 1 : 0);
				}
				
				else if (input[key] is String)
				{
					stream.writeByte(BSP_VAL_STRING);
					// Write length first
					var vs : String = input[key] as String;
					var vsb : ByteArray = new ByteArray();
					vsb.writeUTFBytes(vs);
					
					if (BSPConstants.PACKET_MARK_64B == mark)
					{
						stream.writeInt(0);
					}
					stream.writeUnsignedInt(vsb.length);
					stream.writeBytes(vsb);
				}
				
				else if (input[key] is Array)
				{
					stream.writeByte(BSP_VAL_ARRAY);
					var va : Array = input[key] as Array;
					_ins_stream_array(va, stream, mark);
					stream.writeByte(BSP_VAL_ARRAY_END);
				}
				
				else if (input[key] is Object)
				{
					stream.writeByte(BSP_VAL_OBJECT);
					var vo : Object = input[key] as Object;
					_ins_stream_object(vo, stream, mark);
					stream.writeByte(BSP_VAL_OBJECT_END);
				}
				
				else
				{
					stream.writeByte(BSP_VAL_NULL);
				}
			}
			
			return;
		}
		
		private function _ins_stream_array(input : Array, stream : ByteArray, mark : uint) : void
		{
			for (var key : uint = 0; key < input.length; key ++)
			{
				if (input[key] is int)
				{
					stream.writeByte(BSP_VAL_INT32);
					stream.writeInt(input[key]);
				}
					
				else if (input[key] is Number)
				{
					stream.writeByte(BSP_VAL_DOUBLE);
					stream.writeDouble(input[key]);
				}
					
				else if (input[key] is String)
				{
					stream.writeByte(BSP_VAL_STRING);
					// Write length first
					var vs : String = input[key] as String;
					var vsb : ByteArray = new ByteArray();
					vsb.writeUTFBytes(vs);
					
					if (BSPConstants.PACKET_MARK_64B == mark)
					{
						stream.writeInt(0);
					}
					stream.writeUnsignedInt(vsb.length);
					stream.writeBytes(vsb);
				}
				
				else if (input[key] is Array)
				{
					stream.writeByte(BSP_VAL_ARRAY);
					var va : Array = input[key] as Array;
					_ins_stream_array(va, stream, mark);
					stream.writeByte(BSP_VAL_ARRAY_END);
				}
					
				else if (input[key] is Object)
				{
					stream.writeByte(BSP_VAL_OBJECT);
					var vo : Object = input[key] as Object;
					_ins_stream_object(vo, stream, mark);
					stream.writeByte(BSP_VAL_OBJECT_END);
				}
					
				else
				{
					stream.writeByte(BSP_VAL_NULL);
				}
			}
			
			return;
		}
	}
}